package org.wheel.jpa.query;

import java.lang.reflect.Method;

import javax.persistence.EntityManager;

import org.hibernate.query.Query;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory;
import org.springframework.data.jpa.repository.query.WheelQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.wheel.base.stmt.OrmSqlUtils;
import org.wheel.base.utils.ReflectionUtils;

/**
 * @author klaus.wang
 */
public class BaseJpaQueryLookupStrategy {

	private static final SpelExpressionParser PARSER = new SpelExpressionParser();

	/**
	 * Private constructor to prevent instantiation.
	 */
	private BaseJpaQueryLookupStrategy() {
	}

	/**
	 * Base class for {@link QueryLookupStrategy} implementations that need access
	 * to an {@link EntityManager}.
	 *
	 * @author Oliver Gierke
	 * @author Thomas Darimont
	 */
	private abstract static class AbstractQueryLookupStrategy implements QueryLookupStrategy {

		private final EntityManager em;
		private final JpaQueryMethodFactory queryMethodFactory;

		/**
		 * Creates a new {@link AbstractQueryLookupStrategy}.
		 *
		 * @param em                 must not be {@literal null}.
		 * @param queryMethodFactory must not be {@literal null}.
		 */
		public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory) {

			Assert.notNull(em, "EntityManager must not be null!");
			Assert.notNull(queryMethodFactory, "JpaQueryMethodFactory must not be null!");

			this.em = em;
			this.queryMethodFactory = queryMethodFactory;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(
		 * java.lang.reflect.Method,
		 * org.springframework.data.repository.core.RepositoryMetadata,
		 * org.springframework.data.projection.ProjectionFactory,
		 * org.springframework.data.repository.core.NamedQueries)
		 */
		@Override
		public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
				NamedQueries namedQueries) {
			return resolveQuery(queryMethodFactory.build(method, metadata, factory), em, namedQueries);
		}

		protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em,
				NamedQueries namedQueries);
	}

	/**
	 * {@link QueryLookupStrategy} to create a query from the method name.
	 *
	 * @author Oliver Gierke
	 * @author Thomas Darimont
	 */
	private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy {

		private final EscapeCharacter escape;

		public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
				EscapeCharacter escape) {

			super(em, queryMethodFactory);

			this.escape = escape;
		}

		@Override
		protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
			try {
				return WheelQuery.partTreeJpaQueryConstructor.newInstance(method, em, escape);
			} catch (Exception e) {
				throw new RuntimeException(String.format("JpaQuery methodName #%s# not exist!", method.getName()));
			}
//			return new PartTreeJpaQuery(method, em, escape);
		}
	}

	/**
	 * {@link QueryLookupStrategy} that tries to detect a declared query declared
	 * via {@link Query} annotation followed by a JPA named query lookup.
	 *
	 * @author Oliver Gierke
	 * @author Thomas Darimont
	 * @author Jens Schauder
	 */
	private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {

		private final QueryMethodEvaluationContextProvider evaluationContextProvider;

		/**
		 * Creates a new {@link DeclaredQueryLookupStrategy}.
		 *
		 * @param em
		 * @param queryMethodFactory
		 * @param evaluationContextProvider
		 */
		public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
				QueryMethodEvaluationContextProvider evaluationContextProvider) {

			super(em, queryMethodFactory);

			this.evaluationContextProvider = evaluationContextProvider;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.
		 * AbstractQueryLookupStrategy#resolveQuery(org.springframework.data.jpa.
		 * repository.query.JpaQueryMethod, javax.persistence.EntityManager,
		 * org.springframework.data.repository.core.NamedQueries)
		 */
		@Override
		protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {

//			RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, evaluationContextProvider);
			RepositoryQuery query = (RepositoryQuery) ReflectionUtils.invokeMethod(WheelQuery.fromQueryAnnotation, WheelQuery.INSTANCE,
					method, em, evaluationContextProvider);

			if (null != query) {
				return query;
			}

//			query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
			query = (RepositoryQuery) ReflectionUtils.invokeMethod(WheelQuery.fromProcedureAnnotation, WheelQuery.INSTANCE, method, em);
			if (null != query) {
				return query;
			}

			String name = method.getNamedQueryName();
			if (namedQueries.hasQuery(name)) {
//				return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), evaluationContextProvider);
				query = (RepositoryQuery) ReflectionUtils.invokeMethod(WheelQuery.fromMethodWithQueryString, WheelQuery.INSTANCE, method, em,
						namedQueries.getQuery(name), evaluationContextProvider);
				return query;
			}

//			query = NamedQuery.lookupFrom(method, em);
			query = (RepositoryQuery) ReflectionUtils.invokeMethod(WheelQuery.lookupFrom, WheelQuery.INSTANCE, method, em);

			if (null != query) {

				return query;
			}

			if ((query = getRepositoryQuery(method, em)) != null) {

				return query;
			}

			throw new IllegalStateException(
					String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
		}

		/**
		 * repository sql. hql
		 * 
		 * @param method
		 * @param em
		 * @return
		 */
		protected RepositoryQuery getRepositoryQuery(JpaQueryMethod method, EntityManager em) {
			WheelQuery query = null;
			RepositoryMetadata metadata = (RepositoryMetadata)ReflectionUtils.accessibleFieldValue(method, "metadata");
			String id = String.format("%s.%s", metadata.getRepositoryInterface().getName(), method.getName());
			if (OrmSqlUtils.has(id)) {
				query = new WheelQuery(method, em, id);
				query.setParser(PARSER);
				query.setMethodName(id);
				query.setEvaluationContextProvider(evaluationContextProvider);
			}
			return query;
		}
	}

	/**
	 * {@link QueryLookupStrategy} to try to detect a declared query first (
	 * {@link org.springframework.data.jpa.repository.Query}, JPA named query). In
	 * case none is found we fall back on query creation.
	 *
	 * @author Oliver Gierke
	 * @author Thomas Darimont
	 */
	private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLookupStrategy {

		private final DeclaredQueryLookupStrategy lookupStrategy;
		private final CreateQueryLookupStrategy createStrategy;

		/**
		 * Creates a new {@link CreateIfNotFoundQueryLookupStrategy}.
		 *
		 * @param em                 must not be {@literal null}.
		 * @param queryMethodFactory must not be {@literal null}.
		 * @param createStrategy     must not be {@literal null}.
		 * @param lookupStrategy     must not be {@literal null}.
		 */
		public CreateIfNotFoundQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
				CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy) {

			super(em, queryMethodFactory);

			Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null!");
			Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null!");

			this.createStrategy = createStrategy;
			this.lookupStrategy = lookupStrategy;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.
		 * AbstractQueryLookupStrategy#resolveQuery(org.springframework.data.jpa.
		 * repository.query.JpaQueryMethod, javax.persistence.EntityManager,
		 * org.springframework.data.repository.core.NamedQueries)
		 */
		@Override
		protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {

			try {
				return lookupStrategy.resolveQuery(method, em, namedQueries);
			} catch (IllegalStateException e) {
				return createStrategy.resolveQuery(method, em, namedQueries);
			}
		}
	}

	/**
	 * Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and
	 * {@link Key}.
	 *
	 * @param em                        must not be {@literal null}.
	 * @param queryMethodFactory        must not be {@literal null}.
	 * @param key                       may be {@literal null}.
	 * @param evaluationContextProvider must not be {@literal null}.
	 * @param escape
	 * @return
	 */
	public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
			@Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escape) {

		Assert.notNull(em, "EntityManager must not be null!");
		Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");

		switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
		case CREATE:
			return new CreateQueryLookupStrategy(em, queryMethodFactory, escape);
		case USE_DECLARED_QUERY:
			return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider);
		case CREATE_IF_NOT_FOUND:
			return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
					new CreateQueryLookupStrategy(em, queryMethodFactory, escape),
					new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider));
		default:
			throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
		}
	}
}
